package com.lmxdawn.api.common.session;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.SystemClock;
import cn.hutool.extra.spring.SpringUtil;
import com.lmxdawn.api.admin.util.RedisUtil;
import com.lmxdawn.api.common.session.model.Token;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.Assert;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * 基于Redis实现的用户会话
 *
 * @author jy
 * @since 2023-12-04 14:11
 */
@Slf4j
public class RedisSessionStrategy implements SessionStrategy {
    
    /**
     * 永不过期
     */
    private static final long NEVER_EXPIRE = -1;
    /**
     * 立马过期
     */
    private static final long SOON_EXPIRE = 0;

    private static final String TOKEN_KEY = "bp:login:token:";
    
    private static final String SESSION_KEY = "bp:login:session:";

    private static final RedisUtil redisTool = SpringUtil.getBean(RedisUtil.class);
    
    @Override
    public void login(UserSession userSession, HttpServletRequest request, HttpServletResponse response) {
        Objects.requireNonNull(userSession, "用户会话对象不允许为空");
        Assert.isTrue(ObjectUtils.isNotEmpty(userSession.getToken()), "用户会话Token不允许为空");
        Token token = CollUtil.getFirst(userSession.getToken());
        UserSession userSessionCache = this.getUserSession(userSession.getId());
        List<Token> allTokenList = new ArrayList<>();
        if (Objects.nonNull(userSessionCache)) {
            List<Token> tokenList = this.getTokenList(userSessionCache);
            userSession.setIsConcurrent(Optional.ofNullable(userSession.getIsConcurrent()).orElse(Boolean.FALSE));
            if (!userSession.getIsConcurrent()) {
                // 不允许并发登录
                this.removeToken(tokenList);
            } else {
                allTokenList.addAll(tokenList);
            }
            userSessionCache.setRealName(userSession.isRealName());
            userSessionCache.setTipRead(userSession.isTipRead());
        } else {
            userSessionCache = userSession;
        }
        allTokenList.add(token);
        userSessionCache.setToken(allTokenList);
        String loginId = userSession.getId();
        this.setLoginId(token.getValue(), loginId, token.getTimeout());
        this.setUserSession(loginId, userSessionCache);
    }
    
    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response) {
        String tokenValue = TokenHelper.getTokenValue(request);
        if (StringUtils.isNotEmpty(tokenValue)) {
            UserSession userSession = this.getUserSession(request);
            List<Token> tokenList = this.getTokenList(userSession);
            List<Token> retainTokenList = new ArrayList<>();
            List<Token> deleteTokenList = new ArrayList<>();
            for (Token token : tokenList) {
                if (Objects.equals(tokenValue, token.getValue())) {
                    deleteTokenList.add(token);
                } else {
                    retainTokenList.add(token);
                }
            }
            userSession.setToken(retainTokenList);
            this.removeToken(deleteTokenList);
            if (CollUtil.size(retainTokenList) == 0) {
                this.removeUserSession(userSession.getId());
            } else {
                this.setUserSession(userSession.getId(), userSession, false);
            }
        }
    }
    
    @Override
    public UserSession getUserSession(HttpServletRequest request) {
        String tokenValue = TokenHelper.getTokenValue(request);
        if (StringUtils.isEmpty(tokenValue)) {
            return null;
        }
        String loginId = this.getLoginId(tokenValue);
        if (Objects.isNull(loginId)) {
            return null;
        }
        // 从缓存中获取 UserSession 对象
        UserSession userSession = this.getUserSession1(loginId);
        if (Objects.isNull(userSession)) {
            return null;
        }
        // 自动续签
        this.renewal(userSession, tokenValue);
        return userSession;
    }

    @Override
    public UserSession getUserSession(String tokenValue) {
        if (StringUtils.isEmpty(tokenValue)) {
            return null;
        }
        String loginId = this.getLoginId(tokenValue);
        if (Objects.isNull(loginId)) {
            return null;
        }
        // 从缓存中获取 UserSession 对象
        UserSession userSession = this.getUserSession1(loginId);
        if (Objects.isNull(userSession)) {
            return null;
        }
        return userSession;
    }
    
    @Override
    public void kickout(Object loginId) {
        Objects.requireNonNull(loginId);
        UserSession userSession = this.getUserSession(Convert.toStr(loginId));
        if (Objects.nonNull(userSession)) {
            if (log.isInfoEnabled()) {
                log.info("用户被踢下线：{}", loginId);
            }
            this.removeToken(userSession.getToken());
            this.removeUserSession(loginId);
        }
    }
    
    /**
     * Token 续签
     *
     * @param userSession 用户会话
     * @param tokenValue  token值
     */
    private void renewal(UserSession userSession, String tokenValue) {
        if (Objects.isNull(userSession)) {
            return;
        }
        List<Token> tokenList = this.getTokenList(userSession);
        Token token = this.getToken(tokenList, tokenValue);
        if (Objects.isNull(token)) {
            log.warn("Token续签失败，getToken返回空，用户ID：{}", userSession.getId());
            return;
        }
        long timeout = token.getTimeout();
        long updateTime = token.getUpdateTime();
        long now = SystemClock.now();
        long half = (timeout * 1000) / 2;
        // 系统时间 - 最新更新时间 > 过期时间的一半值
        if ((now - updateTime) > half) {
            tokenList.forEach(t -> {
                if (Objects.equals(tokenValue, t.getValue())) {
                    token.setUpdateTime(now);
                }
            });
            userSession.setToken(tokenList);
            this.setLoginId(tokenValue, userSession.getId(), timeout);
            this.setUserSession(userSession.getId(), userSession);
        }
    }
    
    /**
     * 获取登录账号ID
     *
     * @param tokenValue token值
     * @return
     */
    private String getLoginId(String tokenValue) {

        Object loginId = redisTool.get(getTokenKey(tokenValue));
        return Objects.isNull(loginId) ? null : String.valueOf(loginId);
    }
    
    /**
     * 获取用户会话
     *
     * @param loginId 登录账号ID
     * @return
     */
    private UserSession getUserSession1(String loginId) {
        return redisTool.getObjFromCache(getSessionKey(loginId), UserSession.class);
    }
    
    /**
     * 用户Token
     *
     * @param userSession 用户会话
     * @return
     */
    private List<Token> getTokenList(UserSession userSession) {
        if (Objects.isNull(userSession)) {
            return new ArrayList<>();
        }
        return Optional.ofNullable(userSession.getToken()).orElse(new ArrayList<>());
    }
    
    /**
     * 返回匹配的token
     *
     * @param tokenList  用户所有token
     * @param tokenValue 当前token值
     * @return
     */
    private Token getToken(List<Token> tokenList, String tokenValue) {
        if (ObjectUtils.isEmpty(tokenList) || StringUtils.isEmpty(tokenValue)) {
            return null;
        }
        Optional<Token> f = tokenList.stream().filter(t -> Objects.equals(tokenValue, t.getValue())).findFirst();
        return f.orElse(null);
    }
    
    private void setUserSession(Object loginId, UserSession userSession) {
        this.setUserSession(loginId, userSession, true);
    }
    
    /**
     * 设置UserSession
     *
     * @param loginId       账号ID
     * @param userSession   用户会话
     * @param updateTimeout 是否刷新过期时间
     */
    private void setUserSession(Object loginId, UserSession userSession, boolean updateTimeout) {
        if (Objects.isNull(userSession)) {
            return;
        }
        List<Token> tokenList = this.getTokenList(userSession);
        List<Token> retainTokenList = new ArrayList<>();
        List<Token> deleteTokenList = new ArrayList<>();
        for (Token token : tokenList) {
            long now = SystemClock.now();
            long timeoutMs = token.getTimeout() * 1000;
            long updateTime = token.getUpdateTime();
            if (now > (updateTime + timeoutMs)) {
                deleteTokenList.add(token);
            } else {
                retainTokenList.add(token);
            }
        }
        userSession.setToken(retainTokenList);
        String key = this.getSessionKey(loginId);
        if (updateTimeout) {
            // 从token中获取过期时间最大或者-1
            long timeout = this.getSessionTimeout(retainTokenList);
            if (timeout == SOON_EXPIRE) {
                redisTool.del(key);
                removeToken(retainTokenList);
            } else if (timeout == NEVER_EXPIRE) {
                redisTool.set(key, userSession);
            } else {
                redisTool.set(key, userSession, (int) timeout);
            }
        } else {
            redisTool.set(key, userSession);
        }
        this.removeToken(deleteTokenList);
    }
    
    /**
     * 设置登录账号ID
     *
     * @param tokenValue
     * @param loginId
     * @param timeout
     */
    private void setLoginId(String tokenValue, Object loginId, long timeout) {
        String key = this.getTokenKey(tokenValue);
        if (timeout == NEVER_EXPIRE) {
            redisTool.set(key, String.valueOf(loginId));
        } else {
            redisTool.set(key, String.valueOf(loginId), (int) timeout);
        }
    }
    
    /**
     * 从token中获取最大的过期时间或者-1，做为UserSession的过期时间
     *
     * @param tokenList token值集合
     * @return
     */
    private long getSessionTimeout(List<Token> tokenList) {
        if (ObjectUtils.isEmpty(tokenList)) {
            return SOON_EXPIRE;
        }
        List<Long> timeoutList = tokenList.stream().filter(Objects::nonNull).map(Token::getTimeout).sorted()
                .collect(Collectors.toList());
        long first = Optional.ofNullable(CollUtil.getFirst(timeoutList)).orElse(SOON_EXPIRE);
        if (first == NEVER_EXPIRE) {
            return NEVER_EXPIRE;
        }
        return Optional.ofNullable(CollUtil.getLast(timeoutList)).orElse(SOON_EXPIRE);
    }
    
    /**
     * 删除缓存token
     *
     * @param tokenList token值
     */
    private void removeToken(List<Token> tokenList) {
        if (ObjectUtils.isNotEmpty(tokenList)) {
            redisTool.del(tokenList.stream().map(t -> getTokenKey(t.getValue())).toArray(String[]::new));
        }
    }
    
    /**
     * 删除UserSession
     *
     * @param loginId 登录账号ID
     */
    private void removeUserSession(Object loginId) {
        if (Objects.isNull(loginId)) {
            return;
        }
        redisTool.del(this.getSessionKey(loginId));
    }
    
    private String getTokenKey(Object tokenValue) {
        return TOKEN_KEY + tokenValue;
    }
    
    private String getSessionKey(Object loginId) {
        return SESSION_KEY + loginId;
    }
    
}
